/* 
 * IdUtil.java
 * 
 * Data de criação: 27/03/2010
 * 
 * CORPORATIVO - MTE. 
 *
 */
package br.com.o2solutions.util.reflection;

import java.lang.annotation.Annotation;
import java.lang.reflect.AccessibleObject;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import javax.persistence.EmbeddedId;
import javax.persistence.Id;
import javax.persistence.IdClass;

import net.vidageek.mirror.dsl.ClassController;
import net.vidageek.mirror.dsl.Mirror;
import net.vidageek.mirror.reflect.dsl.ReflectionHandler;

import org.springframework.util.Assert;

import br.com.o2solutions.util.lang.StringUtil;

/**
 * Classe responsável por pesquisar qual propriedade representa o Id (JSR-000220
 * Enterprise JavaBeans 3.0 ) e retornar algumas caracteristicas desta.
 * 
 * @author osvaldeir
 * @since 27/03/2010
 * 
 */
public class IdUtil {

	private final static List<Class<?>> annotations = Arrays
			.asList(new Class<?>[] { Id.class, EmbeddedId.class });

	/**
	 * Construtor
	 */
	private IdUtil() {
	}

	/**
	 * Pesquisa pelas anotacoes Id.class, EmbeddedId.class e IdClass.class
	 * verificando se uma propriedade ou metodo possui uma dessas. Caso
	 * encontre, sera retornado o {@link Map} da propriedade ou metodo.
	 * 
	 * @param Object
	 *            Objeto a ser pesquisado quanto ao id
	 * @return Object Objeto que representa o Id do objeto passado como
	 *         parametro. No caso da Anotacao {@link IdClass} e retornada a
	 *         instancia que esta Annotation referencia como classe Empacotadora
	 *         dos ids. Caso nao seja encontrada nenhuma marcacao que indique o
	 *         id do objeto eh retornado null.
	 * @throws AssertionError
	 *             Caso o objeto informado como parametro seja null
	 */
	public static Object getId(Object object) {
		Object result = null;
		Assert.notNull(object, "O objeto raiz para a extração do Id não pode ser nulo");
		try {
			final Map<String, Object> id = extractId(object);
			final int size = id.size();
			if (size != 1) {
				throw new IllegalArgumentException(
						"Numeros de anotacoes para identificar o id Invalidos. Esperados 1 encontrados "
								.concat(String.valueOf(size)));
			}
			result = id.get(id.keySet().iterator().next());
		} catch (Exception e) {
			throw new RuntimeException(e);
		}
		return result;
	}

	/*
	 * Extrai a chave=valor do metodo ou propriedade que contem o id do objeto
	 * informado como parametro.
	 * 
	 * @param Object Objeto a ser analizado quanto suas anotacoes
	 * 
	 * @return Map<Class<?>, Object> Map que contem o(s) Id(s).
	 */
	private static Map<String, Object> extractId(Object source)
			throws InstantiationException, IllegalAccessException,
			IllegalArgumentException, InvocationTargetException {
		Map<String, Object> result = new HashMap<String, Object>();
		final ClassController<? extends Object> on = new Mirror().on(source
				.getClass());
		final List<Field> fields = on.reflectAll().fields();
		final Iterator<Field> iteratorRoot = fields.iterator();
		while (iteratorRoot.hasNext()) {
			Field next = iteratorRoot.next();
			Map<String, Object> extracted = extract(next, source);
			if (extracted.isEmpty()) {
				Method method = on.reflect().method(
						"get".concat(StringUtil.capitalize(next.getName())))
						.withoutArgs();
				if (method != null)
					extracted = extract(method, source);
			}
			if (!extracted.isEmpty()) {
				result.putAll(extracted);
			}
			if (!iteratorRoot.hasNext()) {
				Class<?> idClass = getIdClass(source.getClass());
				if (idClass != null) {
					extracted = mountIdClassReturn(result, idClass);
					result = extracted;
				}
			}
		}
		return result;
	}

	/*
	 * Retorna o objeto marcado na anotacao IdClass injetado com os valores
	 * constantes na classe anotada.
	 * 
	 * @param result Map Contendo os id's a serem injetados no objeto
	 * 
	 * @param idClass IdClass a ser retornada como result injetada com os
	 * valores do result
	 * 
	 * @throws InstantiationException
	 * 
	 * @throws IllegalAccessException
	 */
	private static Map<String, Object> mountIdClassReturn(
			Map<String, Object> resultParam, Class<?> idClass)
			throws InstantiationException, IllegalAccessException,
			IllegalArgumentException, InvocationTargetException {
		Map<String, Object> result = new HashMap<String, Object>();
		Object newInstance = new Mirror().on(idClass).invoke().constructor()
				.withoutArgs();
		ReflectionHandler<?> onInternal = new Mirror().on(idClass).reflect();
		Iterator<String> getMethodsName = resultParam.keySet().iterator();
		while (getMethodsName.hasNext()) {
			String next = getMethodsName.next();
			Method getMethod = onInternal.method(next).withoutArgs();
			String setMethodName = getMethod.getName().replaceFirst("get",
					"set");
			Method setMethod = onInternal.method(setMethodName).withArgs(
					getMethod.getReturnType());
			if (setMethod == null) {
				throw new IllegalArgumentException("Metodo ".concat(
						setMethodName.concat("("
								+ getMethod.getReturnType().getName() + ")"))
						.concat(" nao encontrado"));
			}
			setMethod.invoke(newInstance, resultParam.get(next));
		}
		result.put(idClass.getName(), newInstance);
		return result;
	}

	/*
	 * Extrai o Map contendo o nome do metodo "get" (JavaBeans 1.00-A spec) -
	 * mesmo que a anotacao esteja em um atributo - que contem a anotacao
	 * Id.class ou EmbeddedId.class.
	 * 
	 * @param accessibleObject Field ou Method que serao analizados para
	 * pesquisa sobre as anotacoes acima expostas.
	 * 
	 * @return Map<Class<?>,Object> Map contendo como chave o tipo da
	 * propriedade ou tipo de retorno do metodo que contem uma das anotacoes
	 * acima e o valor desta.
	 * 
	 * @throws Exception Caso alguma falha ocorra.
	 */
	private static Map<String, Object> extract(
			AccessibleObject accessibleObject, Object source)
			throws IllegalArgumentException, IllegalAccessException,
			InvocationTargetException {
		Map<String, Object> result = new HashMap<String, Object>();
		List<Annotation> annotations = Arrays.asList(accessibleObject
				.getAnnotations());
		Iterator<Annotation> iterator = annotations.iterator();
		while (iterator.hasNext()) {
			Annotation annotation = iterator.next();
			String key = null;
			Object value = null;
			if (IdUtil.annotations.contains(annotation.annotationType())) {
				Method getMethod = null;
				if (accessibleObject instanceof Field) {
					Field field = (Field) accessibleObject;
					key = "get".concat(StringUtil.capitalize(field.getName()));
					getMethod = new Mirror().on(source.getClass()).reflect()
							.method(key).withoutArgs();
				}
				if (accessibleObject instanceof Method) {
					getMethod = (Method) accessibleObject;
					key = getMethod.getName();
				}
				if (getMethod == null)
					throw new IllegalArgumentException("Nao existe o metodo "
							.concat(key.concat("()")).concat(
									" na classe ".concat(accessibleObject
											.getClass().getName())));
				value = getMethod.invoke(source);
				result.put(key, value);
				break;
			}

		}
		return result;
	}

	/*
	 * Verifica se a classe possui a anotacao de IdClass. Neste caso, a classe
	 * informada pode ter mais de um Id
	 * 
	 * @param clazz Classe a ser verificada quanto a anotacao IdClass
	 * 
	 * @return true Caso a classe esteja anotada com IdClass.
	 */
	private static Class<?> getIdClass(Class<?> clazz) {
		Class<?> result = null;
		final IdClass idClass = clazz.getAnnotation(IdClass.class);
		if (idClass != null) {
			result = idClass.value();
		}
		return result;
	}

}
